We will look at four network applications, written completely from scratch in Java. Each of these applications use the client-server paradigm, which we discussed earlier. We’ll use TCP exclusively here. Recall that ports from 49152 to 65535 can be used for anything you want, so we’ll be using these.
Java’s abstraction over the socket API is to use a ServerSocket object that automatically listens, then creates a different socket on accept. Java sockets have input streams and output streams built in, which makes programming rather pleasant.
Four applications are presented in order of increasing complexity:
These applications communicate insecurely.
None of these applications even try to secure communication. All data is sent between hosts completely in the clear. The goal at this point is to illustrate the most basic applications and how they use transport-level services. In real life, use a secure sockets layer.
This is perhaps the simplest possible server. It listens on port 59090. When a client connects, the server sends the current datetime to the client. The connection socket is created in a try-with-resources block so it is automatically closed at the end of the block. Only after serving the datetime and closing the connection will the server go back to waiting for the next client.
/**
* A simple TCP server. When a client connects, it sends the client the current
* datetime, then closes the connection. This is arguably the simplest server
* you can write. It is *so* simple that the client has to be completely served
* its date before the server will be able to handle another client, which is
* not a good paradigm for a production-quality server.
*/
void main() throws IOException {
try (var listener = new ServerSocket(59090)) {
IO.println("The date server is running...");
while (true) {
try (var socket = listener.accept()) {
var out = new PrintWriter(socket.getOutputStream(), true);
out.println(new Date().toString());
}
}
}
}
Discussion:
ServerSocket.accept()
call is a BLOCKING CALL.PrintWriter
, we can specify strings to write, which Java will automatically convert (decode) to bytes.PrintWriter
, in this case true
tells Java to flush automatically after every println
.close
call is required.Run the server:
$ java DateServer.java The date server is running...
To see that is running (you will need a different terminal window):
$ netstat -an | grep 59090 tcp46 0 0 *.59090 *.* LISTEN
Test the server with nc
:
$ nc localhost 59090 Mon Jul 21 07:39:03 PDT 2025
Woah nc
is amazing! Still, let’s see how to write our own client in Java:
/**
* A command line client for the date server. Requires the IP address of the
* server as the sole argument. Exits after printing the response.
*/
void main(String[] args) throws IOException {
if (args.length != 1) {
System.err.println("Pass the server IP as the sole command line argument");
return;
}
try (var socket = new Socket(args[0], 59090)) {
try (var in = new Scanner(socket.getInputStream())) {
IO.println("Server response: " + in.nextLine());
}
}
}
Discussion:
Socket
constructor takes the IP address and port on the server. If the connect request is accepted, we get a socket object to communicate.Scanner
. These are powerful and convenient. In our case we read a line of text from the server with Scanner.nextLine
.Test the client:
$ java DateClient 127.0.0.1 Server response: Mon Jul 21 07:43:47 PDT 2025
The previous example was pretty trivial: it did not read any data from the client, and worse, it served only one client at a time.
This next server receives lines of text from a client and sends back the lines uppercased. It efficiently handles multiple clients at once: When a client connects, the server spawns a thread, dedicated to just that client, to read, uppercase, and reply. The server can listen for and serve other clients at the same time, so we have true concurrency.
import java.io.IO;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.Executors;
/**
* A server program which accepts requests from clients to capitalize strings.
* When a client connects, a new thread is started to handle it. Receiving
* client data, capitalizing it, and sending the response back is all done on
* the thread, allowing much greater throughput because more clients can be
* handled concurrently. The application limits the number of threads via a
* thread pool (otherwise millions of clients could cause the server to run
* out of resources by allocating too many threads).
*/
void main() throws Exception {
try (var listener = new ServerSocket(59898)) {
IO.println("The capitalization server is running...");
try (var pool = Executors.newVirtualThreadPerTaskExecutor()) {
while (true) {
pool.execute(new Capitalizer(listener.accept()));
}
}
}
}
class Capitalizer implements Runnable {
private Socket socket;
Capitalizer(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
IO.println("Connected: " + socket);
try (var in = new Scanner(socket.getInputStream())) {
try (var out = new PrintWriter(socket.getOutputStream(), true)) {
while (in.hasNextLine()) {
out.println(in.nextLine().toUpperCase());
}
}
} catch (Exception e) {
IO.println("Error:" + socket);
} finally {
IO.println("Closed: " + socket);
}
}
}
Discussion:
Runnable
interface; they do their work in their run
method.run
method.run
method has a loop which keeps reading lines from the socket, uppercasing them, then sending them out. Note the wrapping of the socket streams in a Scanner
and a PrintWriter
so that we can work with strings.finally
block closes the socket. We could not use a try-with-resources block here because the socket was created on the main thread.throws IOException
to the run
method signature (because we are implementing it from the Runnable
interface.Before writing a client, let’s test with nc
. Our server reads until standard input is exhausted, so when you are done typing in lines, hit Ctrl+D (clean exit) or Ctrl+C (abort):
$ java CapitalizeServer.java The capitalize server is running... $ nc 127.0.0.1 59898 yeet YEET Seems t'be workin' SEEMS T'BE WORKIN' Привет, мир ПРИВЕТ, МИР
Now for a pretty simple command line client:
void main(String[] args) throws Exception {
if (args.length != 1) {
System.err.println("Pass the server IP as the sole command line argument");
return;
}
try (var socket = new Socket(args[0], 59898)) {
try (var out = new PrintWriter(socket.getOutputStream(), true)) {
try (var in = new Scanner(socket.getInputStream())) {
while (true) {
String line = IO.readln(); // input to send
if (line == null) break; // no more to send, done
out.println(line); // send to server
IO.println(in.nextLine()); // echo response from server
}
}
}
}
}
This client repeatedly reads lines from standard input, sends them to the server, and writes server responses. It can be used interactively:
$ java CapitalizeClient.java localhost hello HELLO bye BYE
Or you can pipe in a file!
$ python3 -c 'for a in "dog rat cat".split(): print(a)' > animals $ java CapitalizeClient.java localhost < animals DOG RAT CAT
Get into groups of two. One student will start a server in one terminal window and a client in another, and start each. The other student will create two terminal windows each running a client. Before any of the three clients send any data to the server, runnetstat
to make sure you see the listening server and all of the client connections. (On a Mac,netstat -an | grep tcp | grep 59898
is useful to see just the good stuff.) Correlate the netstat output with the log messages echoed by the server and client. As data is sent, keep running netstat. Watch the connections go from ESTABLISHED to TIME_WAIT, and then disappear. Make notes of everything that happens; we’ll discuss as a group when everyone’s done.
Here is the server for multiple two-player games. It listens for two clients to connect, and spawns a thread for each: the first is Player X and the second is Player O. The client and server send simple string messages back and forth to each other; messages correspond to the Tic Tac Toe protocol, which I made up for this example.
/**
* A server for a multi-player tic tac toe game. Loosely based on an example in
* Deitel and Deitel’s “Java How to Program” book from the late 1990s. For this
* project I created a new application-level protocol called TTTP (for Tic Tac
* Toe Protocol), which is entirely plain text. The messages of TTTP are:
*
* Client -> Server
* MOVE <n>
* QUIT
*
* Server -> Client
*
* WELCOME <char>
* VALID_MOVE
* OTHER_PLAYER_MOVED <n>
* OTHER_PLAYER_LEFT
* VICTORY
* DEFEAT
* TIE
* MESSAGE <text>
*/
import java.net.Socket;
void main(String[] args) throws Exception {
try (var listener = new ServerSocket(58901)) {
IO.println("Tic Tac Toe Server is Running...");
try (var pool = Executors.newVirtualThreadPerTaskExecutor()) {
while (true) {
Game game = new Game();
pool.execute(game.new Player(listener.accept(), 'X'));
pool.execute(game.new Player(listener.accept(), 'O'));
}
}
}
}
class Game {
// Board cells numbered 0-8, top to bottom, left to right; null if empty
private Player[] board = new Player[9];
// Whose turn it is now
Player currentPlayer;
public boolean hasWinner() {
return (board[0] != null && board[0] == board[1] && board[0] == board[2])
|| (board[3] != null && board[3] == board[4] && board[3] == board[5])
|| (board[6] != null && board[6] == board[7] && board[6] == board[8])
|| (board[0] != null && board[0] == board[3] && board[0] == board[6])
|| (board[1] != null && board[1] == board[4] && board[1] == board[7])
|| (board[2] != null && board[2] == board[5] && board[2] == board[8])
|| (board[0] != null && board[0] == board[4] && board[0] == board[8])
|| (board[2] != null && board[2] == board[4] && board[2] == board[6]);
}
public boolean boardFilledUp() {
return Arrays.stream(board).allMatch(p -> p != null);
}
public synchronized void move(int location, Player player) {
if (player != currentPlayer) {
throw new IllegalStateException("Not your turn");
} else if (player.opponent == null) {
throw new IllegalStateException("You don't have an opponent yet");
} else if (board[location] != null) {
throw new IllegalStateException("Cell already occupied");
}
board[location] = currentPlayer;
currentPlayer = currentPlayer.opponent;
}
/**
* A Player is identified by a character mark which is either 'X' or 'O'. For
* communication with the client the player has a socket and associated Scanner
* and PrintWriter.
*/
class Player implements Runnable {
final char mark;
Player opponent;
final Socket socket;
final Scanner input;
final PrintWriter output;
public Player(Socket socket, char mark) throws IOException {
this.socket = socket;
this.mark = mark;
this.input = new Scanner(socket.getInputStream());
this.output = new PrintWriter(socket.getOutputStream(), true);
}
@Override
public void run() {
try (socket) {
setup();
processCommands();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (opponent != null && opponent.output != null) {
opponent.output.println("OTHER_PLAYER_LEFT");
}
IO.println("Player " + this + " disconnected");
}
}
@Override
public String toString() {
return mark + "@" + socket.getRemoteSocketAddress();
}
private void setup() {
IO.println("Player " + this + " connected");
IO.println("Sending welcome message to " + this);
output.println("WELCOME " + mark);
if (mark == 'X') {
currentPlayer = this;
output.println("MESSAGE Waiting for opponent to connect");
} else {
opponent = currentPlayer;
opponent.opponent = this;
opponent.output.println("MESSAGE Your move");
}
}
private void processCommands() {
while (input.hasNextLine()) {
var command = input.nextLine();
IO.println("Received command from " + this + ": " + command);
if (command.startsWith("QUIT")) {
// No more to read from this player
return;
} else if (command.startsWith("MOVE")) {
processMoveCommand(Integer.parseInt(command.substring(5)));
}
}
}
private void processMoveCommand(int location) {
try {
move(location, this);
output.println("VALID_MOVE");
opponent.output.println("OPPONENT_MOVED " + location);
if (hasWinner()) {
output.println("VICTORY");
opponent.output.println("DEFEAT");
} else if (boardFilledUp()) {
output.println("TIE");
opponent.output.println("TIE");
}
} catch (IllegalStateException e) {
IO.println("Rejected move from " + this + ": " + e.getMessage());
output.println("MESSAGE " + e.getMessage());
}
}
}
}
These days, games like this would be played with clients in a web browser, and the server would be a web server (likely using a WebSockets library). But today, we’re learning about programming directly with sockets, on custom ports, with custom protocols, so we’re sticking with Java for our custom clients. The first version of this program was written in about 2002, so it uses...wait for it...Java Swing!
import module java.desktop;
import java.awt.event.MouseEvent;
/**
* A client for a multi-player tic tac toe game. Loosely based on an example in
* Deitel and Deitel’s “Java How to Program” book from the late 1990s. For this
* project I created a new application-level protocol called TTTP (for Tic Tac
* Toe Protocol), which is entirely plain text. The messages of TTTP are:
*
* Client -> Server
* MOVE <n>
* QUIT
*
* Server -> Client
* WELCOME <char>
* VALID_MOVE
* OTHER_PLAYER_MOVED <n>
* OTHER_PLAYER_LEFT
* VICTORY
* DEFEAT
* TIE
* MESSAGE <text>
*/
public class TicTacToeClient {
private JFrame frame = new JFrame("Tic Tac Toe");
private JLabel messageLabel = new JLabel("...");
private Square[] board = new Square[9];
private Square currentSquare;
private final Socket socket;
private final Scanner in;
private final PrintWriter out;
public TicTacToeClient(String serverAddress) throws Exception {
socket = new Socket(serverAddress, 58901);
in = new Scanner(socket.getInputStream());
out = new PrintWriter(socket.getOutputStream(), true);
}
public void createAndShowGUI() {
// Runs on the EDT
messageLabel.setBackground(Color.lightGray);
frame.getContentPane().add(messageLabel, BorderLayout.SOUTH);
var boardPanel = new JPanel();
boardPanel.setBackground(Color.black);
boardPanel.setLayout(new GridLayout(3, 3, 2, 2));
for (var i = 0; i < board.length; i++) {
final int j = i;
board[i] = new Square();
board[i].addMouseListener(new MouseAdapter() {
public void mousePressed(MouseEvent e) {
currentSquare = board[j];
out.println("MOVE " + j);
}
});
boardPanel.add(board[i]);
}
frame.getContentPane().add(boardPanel, BorderLayout.CENTER);
}
static class Square extends JPanel {
JLabel label = new JLabel();
public Square() {
setBackground(Color.white);
setLayout(new GridBagLayout());
label.setFont(new Font("Arial", Font.BOLD, 40));
add(label);
}
public void setText(char text) {
label.setForeground(text == 'X' ? Color.BLUE : Color.RED);
label.setText(text + "");
}
}
/**
* The main thread of the client will listen for messages from the server.
* The first message will be a "WELCOME" message in which we receive our
* mark. Then we go into a loop listening for any of the other messages,
* and handling each message appropriately. The "VICTORY", "DEFEAT", "TIE",
* and "OTHER_PLAYER_LEFT" messages will ask the user whether or not to play
* another game. If the answer is no, the loop is exited and the server is
* sent a "QUIT" message.
*/
public void play() throws Exception {
try (socket) {
var response = in.nextLine();
final var mark = response.charAt(8);
final var opponentMark = mark == 'X' ? 'O' : 'X';
frame.setTitle("Tic Tac Toe: Player " + mark);
while (in.hasNextLine()) {
response = in.nextLine();
if (response.startsWith("VALID_MOVE")) {
ui(() -> {
messageLabel.setText("Valid move, please wait");
currentSquare.setText(mark);
currentSquare.repaint();
});
} else if (response.startsWith("OPPONENT_MOVED")) {
var loc = Integer.parseInt(response.substring(15));
ui(() -> {
board[loc].setText(opponentMark);
board[loc].repaint();
messageLabel.setText("Opponent moved, your turn");
});
} else if (response.startsWith("MESSAGE")) {
var message = response.substring(8);
ui(() -> {messageLabel.setText(message);});
} else if (response.startsWith("VICTORY")) {
ui(() -> {JOptionPane.showMessageDialog(frame, "Winner Winner");});
break;
} else if (response.startsWith("DEFEAT")) {
ui(() -> {JOptionPane.showMessageDialog(frame, "Sorry you lost");});
break;
} else if (response.startsWith("TIE")) {
ui(() -> {JOptionPane.showMessageDialog(frame, "Tie");});
break;
} else if (response.startsWith("OTHER_PLAYER_LEFT")) {
ui(() -> {JOptionPane.showMessageDialog(frame, "Other player left");});
break;
}
}
// Inform server that we are quitting
out.println("QUIT");
}
// Shutdown after clicking one of the game over dialogs
System.exit(0);
}
private void ui(Runnable action) throws Exception {
SwingUtilities.invokeAndWait(action);
}
}
void main(String[] args) throws Exception {
if (args.length != 1) {
System.err.println("Pass the server IP as the sole command line argument");
return;
}
var client = new TicTacToeClient(args[0]);
SwingUtilities.invokeAndWait(() -> {
client.createAndShowGUI();
client.frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
client.frame.setSize(320, 320);
client.frame.setVisible(true);
client.frame.setResizable(false);
});
client.play();
}
nc
. How awesome is this? Do you feel old school?
Here is a chat server. The server must broadcast recently incoming messages to all the clients participating in a chat. This is done by having the server collect all of the client sockets in a dictionary, then sending new messages to each of them.
import java.io.IOException;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Set;
import java.util.HashSet;
import java.util.Scanner;
import java.util.concurrent.Executors;
/**
* A multithreaded chat room server. When a client connects the server requests
* a screen name by sending the client the text "SUBMITNAME", and keeps
* requesting a name until a unique one is received. After a client submits a
* unique name, the server acknowledges with "NAMEACCEPTED". Then all messages
* from that client will be broadcast to all other clients that have submitted a
* unique screen name. The broadcast messages are prefixed with "MESSAGE".
*
* This is just a teaching example so it can be enhanced in many ways, e.g.,
* better logging. Another is to accept a lot of fun commands, like Slack.
*/
public class ChatServer {
// All client names, so we can check for duplicates upon registration.
private static Set<String> names = new HashSet<>();
// The set of all the print writers for all the clients, used for broadcast.
private static Set<PrintWriter> writers = new HashSet<>();
public static void main(String[] args) throws Exception {
System.out.println("The chat server is running...");
var pool = Executors.newFixedThreadPool(500);
try (var listener = new ServerSocket(59001)) {
while (true) {
pool.execute(new Handler(listener.accept()));
}
}
}
/**
* The client handler task.
*/
private static class Handler implements Runnable {
private String name;
private Socket socket;
private Scanner in;
private PrintWriter out;
/**
* Constructs a handler thread, squirreling away the socket. All the interesting
* work is done in the run method. Remember the constructor is called from the
* server's main method, so this has to be as short as possible.
*/
public Handler(Socket socket) {
this.socket = socket;
}
/**
* Services this thread's client by repeatedly requesting a screen name until a
* unique one has been submitted, then acknowledges the name and registers the
* output stream for the client in a global set, then repeatedly gets inputs and
* broadcasts them.
*/
public void run() {
try {
in = new Scanner(socket.getInputStream());
out = new PrintWriter(socket.getOutputStream(), true);
// Keep requesting a name until we get a unique one.
while (true) {
out.println("SUBMITNAME");
name = in.nextLine();
if (name == null) {
return;
}
synchronized (names) {
if (!name.isBlank() && !names.contains(name)) {
names.add(name);
break;
}
}
}
// Now that a successful name has been chosen, add the socket's print writer
// to the set of all writers so this client can receive broadcast messages.
// But BEFORE THAT, let everyone else know that the new person has joined!
out.println("NAMEACCEPTED " + name);
for (PrintWriter writer : writers) {
writer.println("MESSAGE " + name + " has joined");
}
writers.add(out);
// Accept messages from this client and broadcast them.
while (true) {
String input = in.nextLine();
if (input.toLowerCase().startsWith("/quit")) {
return;
}
for (PrintWriter writer : writers) {
writer.println("MESSAGE " + name + ": " + input);
}
}
} catch (Exception e) {
System.out.println(e);
} finally {
if (out != null) {
writers.remove(out);
}
if (name != null) {
System.out.println(name + " is leaving");
names.remove(name);
for (PrintWriter writer : writers) {
writer.println("MESSAGE " + name + " has left");
}
}
try {
socket.close();
} catch (IOException e) {
}
}
}
}
}
Here’s an old client cobbled together in 2002, using Swing.
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
import java.awt.BorderLayout;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
/**
* A simple Swing-based client for the chat server. Graphically it is a frame
* with a text field for entering messages and a textarea to see the whole
* dialog.
*
* The client follows the following Chat Protocol. When the server sends
* "SUBMITNAME" the client replies with the desired screen name. The server will
* keep sending "SUBMITNAME" requests as long as the client submits screen names
* that are already in use. When the server sends a line beginning with
* "NAMEACCEPTED" the client is now allowed to start sending the server
* arbitrary strings to be broadcast to all chatters connected to the server.
* When the server sends a line beginning with "MESSAGE" then all characters
* following this string should be displayed in its message area.
*/
public class ChatClient {
String serverAddress;
Scanner in;
PrintWriter out;
JFrame frame = new JFrame("Chatter");
JTextField textField = new JTextField(50);
JTextArea messageArea = new JTextArea(16, 50);
/**
* Constructs the client by laying out the GUI and registering a listener with
* the textfield so that pressing Return in the listener sends the textfield
* contents to the server. Note however that the textfield is initially NOT
* editable, and only becomes editable AFTER the client receives the
* NAMEACCEPTED message from the server.
*/
public ChatClient(String serverAddress) {
this.serverAddress = serverAddress;
textField.setEditable(false);
messageArea.setEditable(false);
frame.getContentPane().add(textField, BorderLayout.SOUTH);
frame.getContentPane().add(new JScrollPane(messageArea), BorderLayout.CENTER);
frame.pack();
// Send on enter then clear to prepare for next message
textField.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
out.println(textField.getText());
textField.setText("");
}
});
}
private String getName() {
return JOptionPane.showInputDialog(frame, "Choose a screen name:", "Screen name selection",
JOptionPane.PLAIN_MESSAGE);
}
private void run() throws IOException {
try {
var socket = new Socket(serverAddress, 59001);
in = new Scanner(socket.getInputStream());
out = new PrintWriter(socket.getOutputStream(), true);
while (in.hasNextLine()) {
var line = in.nextLine();
if (line.startsWith("SUBMITNAME")) {
out.println(getName());
} else if (line.startsWith("NAMEACCEPTED")) {
this.frame.setTitle("Chatter - " + line.substring(13));
textField.setEditable(true);
} else if (line.startsWith("MESSAGE")) {
messageArea.append(line.substring(8) + "\n");
}
}
} finally {
frame.setVisible(false);
frame.dispose();
}
}
public static void main(String[] args) throws Exception {
if (args.length != 1) {
System.err.println("Pass the server IP as the sole command line argument");
return;
}
var client = new ChatClient(args[0]);
client.frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
client.frame.setVisible(true);
client.run();
}
}
We’ve covered: